date: 2024.08.26
RestController (Test)
main/java/com/wise > 새로 만들기 > Java 클래스
TestController 생성
package com.wise;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/string")
public String test() {
return "yes";
}
@GetMapping("/json")
public Map<String, Object> testJson() {
Map<String, Object> testMap = new HashMap<>();
testMap.put("A", "1");
testMap.put("B", 2);
return testMap;
}
}
http://localhost:8080/test/string
http://localhost:8080/test/json
main/resources/static > 새로 만들기 > HTML 파일
sample.html 생성
작성
http://localhost:8080/sample.html
package com.wise.module.common.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/v")
public class ViewController {
@GetMapping("/{view}")
public String view(@PathVariable("view") String view) {
return view;
}
}
main/resources/templates > 새로 만들기 > HTML 파일
sample.html 생성
작성
http://localhost:8080/v/sample
application.properties 파일을 yml 파일로 변경
yml 형식이 가독성이 좋기 때문에 사용
내용 변경
spring:
lifecycle:
timeout-per-shutdown-phase: 1m # 서버 안전 종료 타임아웃 1 minute, 20초는 20s
server:
shutdown: graceful # 서버 안전 종료
main/resources/application-local.yml
main/resources/application-dev.yml
main/resources/application-prod.yml
spring:
application:
name: Wise
profiles:
active: local
# active: dev
# active: prod
서버 포트 설정해보기
@PropertySource("classpath:application-${spring.profiles.active}.yml") // local, dev, prod
import org.springframework.beans.factory.annotation.Value;
// 중략
@Value("${spring.profiles.active}")
private String profile;
@GetMapping("/profile")
public String profile() {
return profile;
}
> cd C:\Wise\jdk-22\bin
> keytool -genkey -alias spring -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 4000
> Enter keystore password : t2llocal
server:
port: 8443 # 실제 운영 서버에서는 443 (application-prod.yml에서는 port: 443 설정)
ssl:
key-store: classpath:keystore.p12
key-store-type: PKCS12
key-store-password: t2llocal
key-alias: spring
http.port: 8080 # 실제 운영 서버에서는 80 (application-prod.yml에서는 port: 80 설정)
package com.wise.config;
import io.undertow.servlet.api.SecurityConstraint;
import io.undertow.servlet.api.SecurityInfo;
import io.undertow.servlet.api.TransportGuaranteeType;
import io.undertow.servlet.api.WebResourceCollection;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.undertow.UndertowBuilderCustomizer;
import org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ServletConfiguration {
@Value("${http.port}")
private int httpPort;
@Value("${server.port}")
private int sslPort;
@Bean
public ServletWebServerFactory serverFactory() {
UndertowServletWebServerFactory factory = new UndertowServletWebServerFactory();
factory.addBuilderCustomizers((UndertowBuilderCustomizer) builder -> {
builder.addHttpListener(httpPort, "0.0.0.0");
});
factory.addDeploymentInfoCustomizers(deploymentInfo -> {
deploymentInfo.addSecurityConstraint(
new SecurityConstraint()
.addWebResourceCollection(new WebResourceCollection().addUrlPattern("/*"))
.setTransportGuaranteeType(TransportGuaranteeType.CONFIDENTIAL)
.setEmptyRoleSemantic(SecurityInfo.EmptyRoleSemantic.PERMIT))
.setConfidentialPortManager(exchange -> sslPort);
});
return factory;
}
}
HTTP/2 프로토콜은 SSL(https) 필수
server:
http2:
enabled: true
내장 Tomcat과 외장 Tomcat의 성능 차이는 없다.
Tomcat보다 Undertow가 성능이 좋다.
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-undertow'
// 기타 의존성들...
}
실행 후 로그에 Undertow로 변경 확인
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.mariadb.jdbc:mariadb-java-client'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
main/java/com/wise/config/DatabaseConfiguration.java 생성
package com.wise.config;
import javax.sql.DataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@Configuration
@PropertySource("classpath:application-${spring.profiles.active}.yml") // local, dev, prod
public class DatabaseConfiguration {
@Autowired
private ApplicationContext applicationContext;
@Bean
@ConfigurationProperties(prefix="spring.datasource.hikari")
public HikariConfig hikariConfig() {
return new HikariConfig();
}
@Bean
public DataSource dataSource() throws Exception{
DataSource dataSource = new HikariDataSource(hikariConfig());
// System.out.println(dataSource.toString());
return dataSource;
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources("classpath:mybatis/**/*.xml"));
sqlSessionFactoryBean.setTypeAliasesPackage("com.wise.*");
return sqlSessionFactoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
spring:
datasource:
hikari:
driver-class-name: org.mariadb.jdbc.Driver
jdbc-url: jdbc:mariadb://localhost:3306/wise?characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true
username: root
password: root
connection-test-query: SELECT 1
main/resources/mybatis
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test.Test">
<select id="selectTest" parameterType="HashMap" resultType="HashMap">
SELECT user_id
, user_name
, #{param1}
FROM tcm_user_mst
</select>
</mapper>
package com.wise;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;
@RequiredArgsConstructor
@Repository
public class TestDAO {
private final SqlSessionTemplate sqlSession;
public List<Map<String, Object>> selectTest(Map<String, Object> params) {
return sqlSession.selectList("test.Test.selectTest", params);
}
}
package com.wise;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class TestService {
private final TestDAO testDAO;
public List<Map<String, Object>> selectTest(Map<String, Object> params) {
params.put("param1", "AAA");
return testDAO.selectTest(params);
}
}
package com.wise;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RequiredArgsConstructor
@RestController
@RequestMapping("/test")
public class TestController {
private final TestService testService;
@GetMapping("/mybatis")
public Map<String, Object> mybatis() {
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("result", testService.selectTest(new HashMap<>()));
return resultMap;
}
}
http://localhost:8080/test/mybatis
spring:
output:
ansi:
enabled: always
<?xml version="1.0" encoding="UTF-8"?>
<!-- 60초마다 설정 파일의 변경을 확인 하여 변경시 갱신 -->
<configuration scan="true" scanPeriod="60 seconds">
<!-- 색상 설정 -->
<!-- %clr(PATTERN){faint} -->
<!-- blue, cyan, faint, green, magenta, red, yellow-->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
<!-- log file path -->
<property name="LOG_PATH" value="/logs"/>
<!-- log file name -->
<property name="LOG_FILE_NAME" value="info"/>
<!-- err log file name -->
<property name="ERR_LOG_FILE_NAME" value="error"/>
<!-- pattern -->
<property name="LOG_PATTERN" value="%clr([%-5level]) %clr(%d{yy-MM-dd HH:mm:ss}){magenta} [%thread] %clr([%logger{0}:%line]){cyan} - %msg%n"/>
<!-- Console Appender -->
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- File Appender -->
<appender name="File" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 파일경로 설정 -->
<!--<file>${LOG_PATH}/${LOG_FILE_NAME}.log</file>-->
<fileNamePattern>${LOG_PATH}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 출력패턴 설정-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<!-- Rolling 정책 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- .gz,.zip 등을 넣으면 자동 일자별 로그파일 압축 -->
<fileNamePattern>${LOG_PATH}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}_%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 파일당 최고 용량 kb, mb, gb -->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 일자별 로그파일 최대 보관주기(~일), 해당 설정일 이상된 파일은 자동으로 제거-->
<maxHistory>30</maxHistory>
<!--<MinIndex>1</MinIndex>
<MaxIndex>10</MaxIndex>-->
</rollingPolicy>
</appender>
<!-- 에러의 경우 파일에 로그 처리 -->
<appender name="Error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<file>${LOG_PATH}/${ERR_LOG_FILE_NAME}.log</file>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<!-- Rolling 정책 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- .gz,.zip 등을 넣으면 자동 일자별 로그파일 압축 -->
<fileNamePattern>${LOG_PATH}/${ERR_LOG_FILE_NAME}.%d{yyyy-MM-dd}_%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 파일당 최고 용량 kb, mb, gb -->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!-- 일자별 로그파일 최대 보관주기(~일), 해당 설정일 이상된 파일은 자동으로 제거-->
<maxHistory>60</maxHistory>
</rollingPolicy>
</appender>
<!-- root레벨 설정 -->
<root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="File"/>
<appender-ref ref="Error"/>
</root>
<!-- 특정패키지 로깅레벨 설정 -->
<!-- <logger name="org.apache.ibatis" level="DEBUG" additivity="false"> -->
<!-- <appender-ref ref="CONSOLE"/> -->
<!-- <appender-ref ref="FILE"/> -->
<!-- <appender-ref ref="Error"/> -->
<!-- </logger> -->
</configuration>
@Slf4j
선언log.info("test {}", test) 처럼 {}를 이용하여 치환해서 사용해야한다.
log.info("test {}"+test) 처럼 + 연산자를 사용하면 성능 이슈가 있다.
implementation 'org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16'
# driver-class-name: org.mariadb.jdbc.Driver
# jdbc-url: jdbc:mariadb://localhost:3306/wise?characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true
driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
jdbc-url: jdbc:log4jdbc:mariadb://localhost:3306/wise?characterEncoding=UTF-8&serverTimezone=UTC&allowMultiQueries=true
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0
<!-- log4jdbc 옵션 설정 -->
<logger name="jdbc" level="OFF"/>
<!-- 커넥션 open close 이벤트를 로그로 남긴다. -->
<logger name="jdbc.connection" level="OFF"/>
<!-- SQL문만을 로그로 남기며, PreparedStatement일 경우 관련된 argument 값으로 대체된 SQL문이 보여진다. -->
<logger name="jdbc.sqlonly" level="OFF"/>
<!-- SQL문과 해당 SQL을 실행시키는데 수행된 시간 정보(milliseconds)를 포함한다. -->
<logger name="jdbc.sqltiming" level="DEBUG"/>
<!-- ResultSet을 제외한 모든 JDBC 호출 정보를 로그로 남긴다. 많은 양의 로그가 생성되므로 특별히 JDBC 문제를 추적해야 할 필요가 있는 경우를 제외하고는 사용을 권장하지 않는다. -->
<logger name="jdbc.audit" level="OFF"/>
<!-- ResultSet을 포함한 모든 JDBC 호출 정보를 로그로 남기므로 매우 방대한 양의 로그가 생성된다. -->
<logger name="jdbc.resultset" level="OFF"/>
<!-- SQL 결과 조회된 데이터의 table을 로그로 남긴다. -->
<logger name="jdbc.resultsettable" level="OFF"/>
package com.wise.config;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.spi.FilterReply;
public class LogbackFilter extends Filter<ILoggingEvent> {
@Override
public FilterReply decide(ILoggingEvent event) {
if(event.getMessage().contains("NO_LOG")) { // NO_LOG가 들어간 로그는 출력 안함
return FilterReply.DENY;
} else {
return FilterReply.ACCEPT;
}
}
}
/* NO_LOG */
추가<select id="getUser" parameterType="String" resultType="User">
/* NO_LOG */
SELECT user_id
, user_password AS password
, user_name
FROM tcm_user_mst
WHERE user_id = #{value}
</select>
<filter class="com.wise.config.LogbackFilter"/>
main/java/com/wise/interceptor/LoggerInterceptor.java
package com.wise.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
public class LoggerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 컨트롤러 실행되기 전 수행
log.info("[[[ START {} ]]]", request.getRequestURI());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 컨트롤러 실행된 후 수행
log.info("[[[ END {} ]]] \n", request.getRequestURI());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
throws Exception {
// 뷰 응답 완료 후 수행
}
}
main/java/com/wise/config/WebMvcConfiguration.java 생성
package com.wise.config;
import com.wise.interceptor.LoggerInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry){
// LoggerInterceptor 등록
registry.addInterceptor(new LoggerInterceptor());
}
}
implementation 'org.springframework.boot:spring-boot-starter-aop'
main/java/com/wise/aop/LoggerAspect.java
package com.wise.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class LoggerAspect {
/**
* Around : 대상 메소드의 호출 전후, 예외 발생 등 모든 시점에 적용할 수 있는 어드바이스를 정의합니다. 가장 범용적으로 사용할 수 있는 어드바이스입니다.
* Before : 대상 메소드가 실행되기 전에 적용할 어드바이스를 정의합니다.
* AfterReturning : 대상 메소드가 성공적으로 실행되고 결과값을 반환한 후 적용할 어드바이스를 정의합니다.
* AfterThrowing : 대상 메소드에서 예외가 발생했을 때 적용할 어드바이스를 정의합니다. try/catch문의 catch와 비슷한 역할을 합니다.
* After : 대상 메소드의 정상적인 수행 여부와 상관없이 무조건 실행되는 어드바이스를 정의합니다. 즉, 예외가 발생하더라도 실행되기 때문에 자바의 finally와 비슷한 역할을 합니다.
*/
@Around("execution(* com.wise..*Controller.*(..)) or execution(* com.wise..*Service.*(..)) or execution(* com.wise..*DAO.*(..))")
public Object logPrint(ProceedingJoinPoint joinPoint) throws Throwable {
String type = "";
String name = joinPoint.getSignature().getDeclaringTypeName();
if (name.indexOf("Controller") > -1) {
type = "Controller \t: ";
}
else if (name.indexOf("Service") > -1) {
type = "Service \t\t: ";
}
else if (name.indexOf("DAO") > -1) {
type = "DAO \t\t: ";
}
log.info(type + name + "." + joinPoint.getSignature().getName() + "()");
return joinPoint.proceed();
}
}
// 추가 부분
var mapBody = (Map<String, Object>) body;
mapBody.put("advice", "request");
log.info("Request Body: {}", mapBody);
package com.wise.aop;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Map;
@Slf4j
@ControllerAdvice
public class ReqAdviceController implements RequestBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
// TODO Auto-generated method stub
return inputMessage;
}
@SuppressWarnings("unchecked")
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
var mapBody = (Map<String, Object>) body;
// mapBody.put("advice", "request");
log.info("Request Body: {}", mapBody);
return mapBody;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
// TODO Auto-generated method stub
return body;
}
}
// 추가 부분
if (body instanceof Map) {
var mapBody = (Map<String, Object>) body;
mapBody.put("advice", "response");
return mapBody;
}
package com.wise.aop;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import java.util.Map;
@ControllerAdvice
public class RspAdviceController implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@SuppressWarnings("unchecked")
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body instanceof Map) {
var mapBody = (Map<String, Object>) body;
mapBody.put("advice", "response");
return mapBody;
}
return body;
}
}
package com.wise.aop;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
// 상세한 Exception은 아래 메소드보다 위에 작성해야한다.
// Default Exception
@ExceptionHandler(Exception.class)
public Map<String, Object> defaultExceptionHandler(HttpServletRequest request, Exception exception) {
Map<String, Object> result = new HashMap<>();
result.put("ERR_CODE", "100");
result.put("ERR_MSG", exception.getMessage());
log.error("Exception", exception);
return result;
}
}
/*
예외 발생 시 화면으로 표시하고 싶은 경우 아래 사용
@ControllerAdvice
public class ExceptionHandler {
// 상세한 Exception은 아래 메소드보다 위에 작성해야한다.
// Default Exception
@ExceptionHandler(Exception.class)
public String defaultExceptionHandler(HttpServletRequest request, Exception exception) {
log.error("Exception", exception);
return "error/error_default";
}
}
*/
package com.wise.aop;
public class CustomException extends RuntimeException {
private final int ERR_CODE;
public CustomException(String msg){
super(msg);
ERR_CODE = 100;
}
public CustomException(String msg, int errCode){
super(msg);
ERR_CODE = errCode;
}
public int getErrCode() {
return ERR_CODE;
}
}
@ExceptionHandler(CustomException.class)
public Map<String, Object> customExceptionHandler(HttpServletRequest request, CustomException ce) {
Map<String, Object> result = new HashMap<>();
result.put("ERR_CODE", ce.getErrCode());
result.put("ERR_MSG", ce.getMessage());
log.error(String.valueOf(ce.getErrCode()));
log.error(ce.getMessage());
return result;
}
throw new CustomException("Test Custom Exception");
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
<h1>Error</h1>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Error</title>
</head>
<body>
<h1>권한이 없습니다.</h1>
</body>
</html>
package com.wise.auth;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class CustomErrorController implements ErrorController {
// View 요청 시
@RequestMapping(value = "/error", produces = MediaType.TEXT_HTML_VALUE)
public String errorHtml(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if(status != null){
int statusCode = Integer.valueOf(status.toString());
if(statusCode == HttpStatus.NOT_FOUND.value()) {
return "error/error_default";
}
// 권한 없을 경우
if(statusCode == HttpStatus.FORBIDDEN.value() || statusCode == HttpStatus.UNAUTHORIZED.value()) {
//return "redirect:/login";
return "error/error_auth";
}
}
return "error/error_default";
}
// AJAX 요청 시
@RequestMapping("/error")
public ResponseEntity<Void> error(HttpServletRequest request) {
String status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE).toString();
return new ResponseEntity<Void>(HttpStatus.valueOf(Integer.parseInt(status)));
}
}
@EnableTransactionManagement
...
@Bean
public PlatformTransactionManager transactionManager() throws Exception {
return new DataSourceTransactionManager(dataSource());
}
TestController.java
package com.wise.module.common.util;
import java.util.HashMap;
public class CommonMap extends HashMap<String, Object> {
public String getString(Object key) {
Object value = super.get(key);
return (value != null) ? String.valueOf(value) : null;
}
}
package com.wise.module.common.util;
import org.springframework.jdbc.support.JdbcUtils;
import java.util.HashMap;
public class ResultMap extends HashMap<String, Object> {
@Override
public Object put(String key, Object value) {
// 결과 값을 Camel Case로 변환
return super.put(JdbcUtils.convertUnderscoreNameToPropertyName(key), value);
}
}
package com.wise.module.common.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Component
public class BeanUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
BeanUtil.context = context;
}
public static Object getBean(String sBeanName) {
return BeanUtil.context.getBean(sBeanName);
}
public static Method getMethod(Object bean, String methodName) {
Method[] methods = bean.getClass().getMethods();
for (int i = 0; i < methods.length; i++) {
if (methods[i].getName().equals(methodName)) {
return methods[i];
}
}
throw new RuntimeException(methodName + " is not exist.");
}
}
package com.wise.module.common.controller;
import com.wise.aop.CustomException;
import com.wise.module.common.util.BeanUtil;
import com.wise.module.common.util.CommonMap;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
@CrossOrigin
@RestController
@RequestMapping("/api")
public class ServiceController {
@PostMapping("/{service}/{method}")
public Map<String, Object> svcCtr(
@RequestBody CommonMap params,
HttpServletRequest req,
HttpServletResponse res,
@PathVariable("service") String service,
@PathVariable("method") String method) throws Exception {
var result = new CommonMap();
if("null".equals(method)) return result;
Object bean = BeanUtil.getBean(service.concat("Service"));
Method action = BeanUtil.getMethod(bean, method);
try {
result = (CommonMap) action.invoke(bean, params, req, res);
}catch(InvocationTargetException ite) {
if (ite.getCause() instanceof CustomException) {
CustomException ce = (CustomException) ite.getCause();
throw new CustomException(ce.getMessage(), ce.getErrCode());
} else {
Exception e = (Exception) ite.getCause();
throw new Exception(e.getMessage());
}
}
return result;
}
}
package com.wise.module.common.dao;
import com.wise.module.common.util.CommonMap;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public class CommonDAO {
@Autowired
private SqlSessionTemplate sqlSession;
public List<CommonMap> selectList(String sqlId, CommonMap params) {
return sqlSession.selectList(sqlId, params);
}
public CommonMap selectOne(String sqlId, CommonMap params) {
return sqlSession.selectOne(sqlId, params);
}
public String selectString(String sqlId, CommonMap params) {
return sqlSession.selectOne(sqlId, params);
}
public int selectInt(String sqlId, CommonMap params) {
return sqlSession.selectOne(sqlId, params);
}
public int insert(String sqlId, CommonMap params) {
return sqlSession.insert(sqlId, params);
}
public int update(String sqlId, CommonMap params) {
return sqlSession.update(sqlId, params);
}
public int delete(String sqlId, Optional<CommonMap> params) {
return sqlSession.delete(sqlId, params);
}
}
package com.wise.module.common.service;
import com.wise.module.common.dao.CommonDAO;
import org.springframework.beans.factory.annotation.Autowired;
public class CommonService {
@Autowired
protected CommonDAO commonDAO;
}
package com.wise.module.adm;
import com.wise.module.common.service.CommonService;
import com.wise.module.common.util.CommonMap;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;
@Service
public class AdminTestService extends CommonService {
public CommonMap selectAdminList(@RequestBody CommonMap params, HttpServletRequest req, HttpServletResponse res) {
params.put("param1", "AAA");
var list = commonDAO.selectList("test.Test.selectTest", params);
var result = new CommonMap();
result.put("list", list);
return result;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="test.Test">
<select id="selectTest" parameterType="HashMap" resultType="ResultMap">
SELECT user_id
, user_name
, #{param1}
FROM tcm_user_mst
</select>
</mapper>
fetch('http://localhost:8080/api/adminTest/selectAdminList', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify({})
});
http://localhost:8080/v/sample
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
security:
jwt:
access:
token:
secret-key: access-token-key-wise-ver-2-2024 # JWT Access Token
expire: 86400000 # 24 Hour (60*60*24*1000)
package com.wise.auth;
public record User(
String userId,
String password
) {}
package com.wise.auth;
import com.wise.module.common.util.CommonMap;
import lombok.RequiredArgsConstructor;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;
@RequiredArgsConstructor
@Repository
public class UserDAO {
private final SqlSessionTemplate sqlSession;
public User getUser(String username) {
return sqlSession.selectOne("auth.User.getUser", username);
}
public int insertUser(CommonMap param) {
return sqlSession.insert("auth.User.insertUser", param);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="auth.User">
<select id="getUser" parameterType="String" resultType="User">
SELECT user_id
, user_password AS password
, user_name
FROM tcm_user_mst
WHERE user_id = #{value}
</select>
<select id="getMaxPersonNoAndUserSid" resultType="ResultMap">
SELECT MAX(person_no) AS person_no
, MAX(user_sid) AS user_sid
FROM tcm_user_mst
</select>
<insert id="insertUser" parameterType="HashMap">
INSERT INTO tcm_user_mst
(
user_id
, user_password
, person_no
, user_sid
, user_name
, company_code
, department_code
)
VALUES
(
#{userId}
, #{password}
, #{personNo}
, #{userSid}
, #{userName}
, #{companyCode}
, #{departmentCode}
)
</insert>
</mapper>
package com.wise.auth;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserDAO userDAO;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user;
try {
user = userDAO.getUser(username);
} catch (Exception e) {
throw new UsernameNotFoundException("Invalid ID");
}
return new org.springframework.security.core.userdetails.User(user.userId(), user.password(), new ArrayList<>());
}
}
package com.wise.auth;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.UUID;
@Getter
@Component
public class JwtTokenProvider {
private final SecretKey secretKey;
private final long expire;
private final String cookieName;
@Autowired
private CustomUserDetailsService userDetailsService;
public JwtTokenProvider(@Value("${security.jwt.access.token.secret-key}") String secretKey,
@Value("${security.jwt.access.token.expire}") long expire) {
byte[] keyBytes = secretKey.getBytes();
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
this.expire = expire;
this.cookieName = "ACCESS_TOKEN";
}
// 토큰생성
public String createToken(String subject) {
Date now = new Date();
Date validity = new Date(now.getTime() + expire);
String identify = UUID.randomUUID().toString();
return Jwts.builder()
.subject(subject) // 주제 설정
.issuedAt(now) // 토큰 발행 시간 설정
.expiration(validity) // 만료 시간 설정
.claim("Identify", identify) // 식별자 설정
.signWith(this.secretKey) // 키를 사용하여 서명
.compact();
}
// 유효한 토큰인지 확인
public boolean validateToken(String token) {
if(token == null) return false;
try {
Jwts.parser()
.verifyWith(this.secretKey)
.build()
.parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
// 유효한 identify인지 확인
public boolean validateIdentify(String token, String identify) {
if(token == null) return false;
return identify.equals(getIdentify(token));
}
// 토큰에서 값 추출
public String getSubject(String token) {
return Jwts.parser()
.verifyWith(this.secretKey)
.build()
.parseSignedClaims(token)
.getPayload().getSubject();
}
// 토큰에서 identify claim 값 추출
public String getIdentify(String token) {
return Jwts.parser()
.verifyWith(this.secretKey)
.build()
.parseSignedClaims(token)
.getPayload()
.get("Identify", String.class);
}
// 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token, HttpServletRequest request) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getSubject(token));
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return usernamePasswordAuthenticationToken;
}
}
package com.wise.auth;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@RequiredArgsConstructor
@Component
public class JwtCookieUtil {
private final JwtTokenProvider jwtTokenProvider;
public Cookie createJwtCookie(String cookieName, String value){
Cookie token = new Cookie(cookieName, value);
token.setHttpOnly(true);
token.setMaxAge((int)jwtTokenProvider.getExpire());
token.setPath("/");
return token;
}
public Cookie createIdentifyCookie(String value){
Cookie token = new Cookie("Identify", value);
token.setHttpOnly(true);
token.setPath("/");
return token;
}
public Cookie getCookie(HttpServletRequest req, String cookieName){
final Cookie[] cookies = req.getCookies();
if(cookies == null) return null;
for(Cookie cookie : cookies){
if(cookie.getName().equals(cookieName))
return cookie;
}
return null;
}
public Cookie destroyCookie(String cookieName) {
Cookie token = new Cookie(cookieName,"");
token.setHttpOnly(true);
token.setMaxAge(0);
token.setPath("/");
return token;
}
}
package com.wise.auth;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
@Component
public class JwtFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final JwtCookieUtil jwtCookieUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 쿠키에서 토큰 받기
final Cookie jwtCookie = jwtCookieUtil.getCookie(request, jwtTokenProvider.getCookieName());
final Cookie identifyCookie = jwtCookieUtil.getCookie(request, "Identify");
try {
if(jwtCookie != null && identifyCookie != null) {
String token = jwtCookie.getValue();
String identify = identifyCookie.getValue();
// 유효한 토큰인지 확인
if ( jwtTokenProvider.validateToken(token) && jwtTokenProvider.validateIdentify(token, identify) ) {
// 토큰으로부터 유저 정보 받기
Authentication authentication = jwtTokenProvider.getAuthentication(token, request);
// SecurityContext 에 Authentication 객체를 저장합니다.
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (ExpiredJwtException e) {
}
filterChain.doFilter(request, response);
}
}
package com.wise.config;
import com.wise.auth.JwtFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration {
private final JwtFilter jwtFilter;
// 비밀번호 암호화 Bean 등록
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.httpBasic(AbstractHttpConfigurer::disable) // security에서 기본으로 생성하는 login페이지 사용 안함
.csrf(AbstractHttpConfigurer::disable) // csrf 보안 토큰 disable 처리
.authorizeHttpRequests(requests -> { requests
.requestMatchers("/api/**", "/v/**").authenticated() // /api, /v는 인증 필요
.anyRequest().permitAll(); // 그 외 요청은 인증 필요 없음
})
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) // iframe 에러 해결
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); // JwtFilter를 UsernamePasswordAuthenticationFilter 전에 넣는다
return http.build();
}
}
package com.wise.auth;
import com.wise.aop.CustomException;
import com.wise.module.common.util.CommonMap;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
@Slf4j
@RequiredArgsConstructor
@Controller
public class AuthController {
private final JwtTokenProvider jwtTokenProvider;
private final JwtCookieUtil jwtCookieUtil;
private final PasswordEncoder passwordEncoder;
private final UserDAO userDAO;
public boolean validateToken(HttpServletRequest req) {
// 쿠키에서 토큰 받기
final Cookie jwtCookie = jwtCookieUtil.getCookie(req, jwtTokenProvider.getCookieName());
final Cookie identifyCookie = jwtCookieUtil.getCookie(req, "Identify");
if(jwtCookie != null && identifyCookie != null) {
String token = jwtCookie.getValue();
String identify = identifyCookie.getValue();
// 유효한 토큰인지 확인
if ( jwtTokenProvider.validateToken(token) && jwtTokenProvider.validateIdentify(token, identify) ) {
return true;
}
}
return false;
}
@GetMapping("/")
public String index() {
return "redirect:/login";
}
@GetMapping("/main")
public String main(HttpServletRequest req, HttpServletResponse res) {
if(!validateToken(req)) {
return "redirect:/logOut";
}
return "forward:/main.html";
}
@GetMapping("/login")
public String login(HttpServletRequest req, HttpServletResponse res) {
if(validateToken(req)) {
return "redirect:/main";
}
return "login";
}
@GetMapping("/logOut")
public String logout(HttpServletRequest req, HttpServletResponse res) {
Cookie destroyJwtCookie = jwtCookieUtil.destroyCookie(jwtTokenProvider.getCookieName());
Cookie destroyIdentifyCookie = jwtCookieUtil.destroyCookie("Identify");
res.addCookie(destroyJwtCookie);
res.addCookie(destroyIdentifyCookie);
return "redirect:/login";
}
@PostMapping("/auth")
@ResponseBody
public CommonMap auth(@RequestBody CommonMap params, HttpServletRequest req, HttpServletResponse res) {
User member = userDAO.getUser(params.getString("userId"));
if( member == null || !passwordEncoder.matches(params.getString("password"), member.password()) ) {
throw new CustomException("Invalid ID/Password");
}
String token = jwtTokenProvider.createToken(member.userId());
Cookie accessToken = jwtCookieUtil.createJwtCookie(jwtTokenProvider.getCookieName(), token);
Cookie identify = jwtCookieUtil.createIdentifyCookie(jwtTokenProvider.getIdentify(token));
res.addCookie(accessToken);
res.addCookie(identify);
return new CommonMap();
}
@PostMapping("/join")
@ResponseBody
public CommonMap join(@RequestBody CommonMap params, HttpServletRequest req, HttpServletResponse res) {
if(ObjectUtils.isEmpty(params.get("userId")) || ObjectUtils.isEmpty(params.getString("password"))) {
throw new CustomException("Input ID/PW");
}
User member = userDAO.getUser(params.getString("userId"));
if(member != null) {
throw new CustomException("Exist ID");
} else {
params.put("password", passwordEncoder.encode(params.getString("password")));
userDAO.insertUser(params);
String token = jwtTokenProvider.createToken(params.getString("userId"));
Cookie accessToken = jwtCookieUtil.createJwtCookie(jwtTokenProvider.getCookieName(), token);
Cookie identify = jwtCookieUtil.createIdentifyCookie(jwtTokenProvider.getIdentify(token));
res.addCookie(accessToken);
res.addCookie(identify);
}
return new CommonMap();
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<title>Title</title>
</head>
<body>
<h1>Login</h1>
<div id="divId">
ID : <input id="userId" type="text"><br>
PW : <input id="password" type="text"><br>
<br>
<button id="btn" type="button">로그인</button>
<button id="btn2" type="button">가입</button>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
document.querySelector('#btn').addEventListener('click', (e) => {
fetch('/auth', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify({
userId: document.querySelector('#userId').value,
password: document.querySelector('#password').value,
}),
}).then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Network response was not ok.');
}).then((response) => {
if(response.ERR_MSG) {
console.error(response.ERR_MSG);
} else {
location.replace("/main");
}
}).catch((error) => {
console.error('There has been a problem with your fetch operation:', error);
});
});
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
<title>Main</title>
</head>
<body>
<h1>Main</h1>
<div id="divId">
<button id="btn" type="button">로그아웃</button>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
document.querySelector('#btn').addEventListener('click', (e) => {
location.replace("/logOut");
});
});
</script>
</body>
</html>
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
management:
endpoints:
web:
exposure:
include: prometheus, health, info
prometheus:
enabled: true
metrics:
enabled: true
tags:
application: ${spring.application.name}
- job_name: "spring-actuator"
metrics_path: '/actuator/prometheus'
static_configs:
- targets: ['localhost:8080']
C:\Wise\prometheus-2.53.2\prometheus.exe 실행
http://localhost:9090/ 접속
C:\Wise\grafana-v11.2.0\bin\grafana-server.exe 실행
http://localhost:3000 접속
admin / admin
Add your first data source 선택
Prometheus 선택
프로메테우스 서버 주소 입력 ( http://localhost:9090 )
저장
spring boot 검색
Spring Boot 2.1 System Monitor 선택
Copy ID to clipboard 선택
http://localhost:3000 에서 Dashboard > New 버튼 > Import 선택
복사한 대시보드 ID 붙여넣기 후 Load 클릭
Prometheus 선택 후 Import 클릭
Dashboard 선택
완료
> cd C:\Wise\loki-3.0.1
> curl -O -L "https://raw.githubusercontent.com/grafana/loki/v3.0.1/cmd/loki/loki-local-config.yaml"
Assets > loki-windows-amd64.exe.zip 다운로드
> cd C:\Wise\loki-3.0.1
> loki-windows-amd64 -config.file=loki-local-config.yaml
> cd C:\Wise\loki-3.0.1
> curl -O -L "https://raw.githubusercontent.com/grafana/loki/v3.0.1/clients/cmd/promtail/promtail-local-config.yaml"
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
clients:
- url: http://127.0.0.1:3100/loki/api/v1/push # localhost -> 127.0.0.1
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: wiselogs
__path__: C:\logs\info*.log # 로그 파일의 위치를 작성
stream: stdout
> cd C:\Wise\loki-3.0.1
> promtail-windows-amd64 -config.file=promtail-local-config.yaml
Loki 추가
로키 서버 주소 입력 ( http://localhost:3100 ) 후 저장
Explore > Loki 선택
promtail-local-config.yaml 에서 설정한 job 선택
Time은 수집시간이므로 off
Line contains에서 문자열 검색
Run query 버튼 클릭
spring:
devtools:
livereload:
enabled: true
restart:
enabled: false
thyemleaf:
cache: false